package com.itheima;

public class Redis笔记_含高级篇分布式缓存 {
    /*
    Redis是一个基于内存的key-value结构的非关系型数据库。

    注意:虽然Redis是将键值对存储在内存中,但是断电后并不会丢失,原因是关闭服务时自动存储到了硬盘中进行了持久化!

    基于内存存储，读写性能高
    适合存储热点数据（热点商品、资讯、新闻）
    企业应用广泛

    Redis是一个开源的内存中的数据结构存储系统，它可以用作：数据库、缓存和消息中间件。
    Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库，官方提供的数据是可以达到100000+的QPS（每秒内查询次数）。它存储的value类型比较丰富，也被称为结构化的NoSql数据库。

    🔴 NoSql（Not Only SQL），不仅仅是SQL，泛指非关系型数据库。NoSql数据库并不是要取代关系型数据库，而是关系型数据库的补充。

    🔴 非关系型数据库中没有表的概念,也就没有外键约束之类的关系,数据往往以键值对存储,数据之间是没有关系的

    Redis 应用场景:
    缓存
    任务队列
    消息队列
    分布式锁
     */
    /**
     * 关系型数据库(RDBMS):
     * Mysql
     * Oracle
     * DB2
     * SQLServer
     * 非关系型数据库(NoSql):
     * Redis
     * Mongo db
     * MemCached
     */
    /*
    Redis 默认端口号为6379
    redis-server.exe 启动服务器
    redis-cli.exe 打开命令行客户端(Ctrl+L清屏)

    有关配置文件中的项目:
    Redis启动时无需密码校验,可以在redis.conf配置文件中找到requirepass 后面就是密码,可以修改保存,默认该行被注释无需密码(Linux下成功,Windows不成功);需要配合配置文件中的protected-mode yes/no 使用,保护模式开需要密码,保护模式关无需密码


    Redis服务器默认只能本地访问,想要远程访问可以修改.conf配置文件中的bind 127.0.0.1,将该行注释掉,即可远程通过ip、port连接该Redis服务器,例如虚拟机Linux中启动Redis服务器,外机Windows Powershell中使用指令.\redis-cli.exe -h 192.168.138.100 -p 6379 [-a 密码123456]连接虚拟机中的Redis服务器

    Redis默认有16个数据库0-15,连接本地Redis服务器时,默认打开的是0号数据库,
    可以使用select 1切换带1号数据库
    默认数据库数量可以在redis.windows.conf中修改'database 16'的值
     */
    /**
     Redis 数据库中数据的类型

     Redis存储的是key-value结构的数据，其中key固定是字符串类型

     value有5种常用的数据类型：
     字符串----string -- 常用
     哈希------hash -- 适合存储对象,本身就是多个键值对(field1-value1,field2-value2)
     列表------list -- 按照插入的顺序排列,可以用重复元素
     集合------set -- 无序集合,没有重复元素
     有序集合--sorted set (aka Zset因为指令都以Z开头) -- 有序集合,每个元素都是键值对,值是分值score,元素默认按照score升序排序,没有重复元素(最典型的应用是排行榜)


     在Redis中，有五种主要的数据类型，每种类型都有不同的特点和用途。这些数据类型包括：

     1. String（字符串）：
     String是最基本的数据类型，在Redis中存储的是二进制安全的字符串。String可以存储普通的文本数据，也可以存储序列化的对象。常用的操作包括设置值、获取值、追加、计数器等。

     2. List（列表）：
     List是一个有序的字符串列表，它可以包含重复的元素。可以在列表的两端进行元素的插入和删除操作，支持类似于栈和队列的操作。

     3. Set（集合）：
     Set是一个无序的字符串集合，它不允许包含重复的元素。可以进行集合的交集、并集和差集等操作。

     4. Hash（哈希表）：
     Hash是一个键值对集合，类似于Java中的Map。其中的键和值都是字符串类型，适合存储对象的属性。

     5. Sorted Set（有序集合）：
     Sorted Set是一个有序的字符串集合，每个元素都关联着一个分数，用于排序。不允许包含重复的元素，但可以根据分数来进行范围查找。

     这些数据类型使得Redis成为一个功能丰富的数据存储解决方案，可以用于各种场景，例如缓存、计数器、排行榜、发布/订阅等。通过灵活地使用这些数据类型，可以最大程度地发挥Redis的优势。
     */


//Redis 常用命令
//字符串 string 操作命令
//SET key value			设置指定key的值
//GET key			获取指定key的值
//SETEX key seconds value	设置指定key的值，并将 key 的过期时间设为 seconds 秒(EXpire)
//SETNX key value		只有在 key 不存在时才会设置 key 的值,存在则不赋值(Not eXist)
/*
注意点:
get 不存在的key 返回空值nil
后set的值会覆盖先前set的值
 */


//哈希 hash 操作命令
//Redis hash 是一个string类型的 field 和 value 的映射表，一个key对应多组field-value,hash特别适合用于存储对象，常用命令：
//HSET key field value 	将哈希表 key 中的字段 field 的值设为 value
//HGET key field 		获取存储在哈希表中指定字段的值
//HDEL key field		删除存储在哈希表中的指定字段
//HKEYS key 		获取哈希表中所有字段
//HVALS key 		获取哈希表中所有值
//HGETALL key 		获取在哈希表中指定 key 的所有字段和值
/*
key  -------> {    value     }
              {field1  value1}
              {field2  value2}
              {  ...    ...  }
 */


//列表 list 操作命令
//Redis 列表是简单的字符串列表，按照插入顺序排序，常用命令：
//LPUSH key value1 [value2] 	将一个或多个值插入到列表头部
//LRANGE key start stop 		获取列表指定范围内的元素(start == 0 && stop == -1表示查询所有)
//RPOP key 			移除并获取列表最后一个元素(R代表Right,可以换成L(eft))
//LLEN key 			获取列表长度
//BRPOP key1 [key2 ] timeout 	移出并获取列表的最后一个元素， 如果列表没有元素会阻塞列表直到等待超时,或发现可弹出元素为止(Block)
    /*
    key ---->[   value   ]
             [a,b,c,d,...]
     */


//集合 set 操作命令
//Redis set 是string类型的无序集合。集合成员是唯一的，这就意味着集合中不能出现重复的数据，常用命令：
//SADD key member1 [member2] 	向集合添加一个或多个成员
//SMEMBERS key 		返回集合中的所有成员
//SCARD key 			获取集合的成员数
//SINTER key1 [key2] 		返回给定所有集合的交集
//SUNION key1 [key2] 		返回所有给定集合的并集
//SDIFF key1 [key2] 		返回给定所有集合的差集
//SREM key member1 [member2] 	移除集合中一个或多个成员
/*
key ---> {   value   }
         {  b   a  c }
 */

//有序集合 sorted set (aka Zset)操作命令
//Redis sorted set 有序集合是 string 类型元素的集合，且不允许重复的成员。每个元素都会关联一个double类型的分数(score) 。redis正是通过分数来为集合中的成员进行从小到大排序(默认升序)。有序集合的成员是唯一的，但分数却可以重复。
//常用命令：
//ZADD key score1 member1 [score2 member2] 	向有序集合添加一个或多个成员，或者更新已存在成员的分数
//ZRANGE key start stop [WITHSCORES] 		通过索引区间返回有序集合中指定区间内的成员按升序排序显示,降序用ZREVRANGE key start stop [WITHSCORES](start == 0 && stop == -1表示查询所有)
//ZINCRBY key increment member 			有序集合中对指定成员的分数加上增量 increment
//ZREM key member [member ...] 			移除有序集合中的一个或多个成员
    /*
    key -------> {value}
                 {  a  }:1.1
                 {  b  }:2.2
                 {  c  }:3.3
     */


//通用命令
//KEYS pattern 		查找所有符合给定模式( pattern)的key   (keys * 查全部,keys abc* 查以abc开头的key)
//EXISTS key 		检查给定 key 是否存在
//TYPE key 		返回 key 所储存的值的类型
//TTL key 		返回给定 key 的剩余生存时间(TTL, time to live)，以秒为单位
//DEL key 		该命令用于在 key 存在是删除 key
//Expire KEY_NAME TIME_IN_SECONDS	seconds 为给定 key 设置过期时间。
//PERSIST KEY_NAME	移除 key 的过期时间，key 将持久保持。

//    更多命令查阅https://www.redis.net.cn/order/


/**
 Redis 高级 ---- 分布式缓存
 */
/*
单点Redis的问题:
1.数据丢失问题,Redis是内存存储，服务重启可能会丢失数据
2.并发能力问题,单节点Redis并发能力虽然不错，但也无法满足如618这样的高并发场景
3.故障恢复问题,如果Redis宕机，则服务不可用，需要一种自动的故障恢复手段
4.存储能力问题,Redis基于内存，单节点能存储的数据量难以满足海量数据需求

解决方案:
1.数据丢失问题:实现Redis数据持久化
2.并发能力问题:搭建主从集群，实现读写分离
3.故障恢复问题:利用Redis哨兵，实现健康检测和自动恢复
4.存储能力问题:搭建分片集群，利用插槽机制实现动态扩容
*/

/*
Redis有两种持久化方案：
- RDB持久化
- AOF持久化
*/

/*
Redis持久化方案一:

RDB全称Redis Database Backup file（Redis数据备份文件），也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后，从磁盘读取快照文件，恢复数据。快照文件称为RDB文件，默认是保存在当前运行目录。

------------------------------------------------------------

save命令(一般最后不使用Redis才执行一次)
在redis-cli中执行save命令，可以立即执行一次RDB
save命令会导致主进程执行RDB，这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。

bgsave命令
可以异步执行RDB
这个命令执行后会开启独立进程完成RDB，主进程可以持续处理用户请求，不受影响。

Redis停机时会执行一次save命令，实现RDB持久化。

------------------------------------------------------------

Redis内部有触发RDB的机制，可以在redis.conf文件中找到，格式如下：

save 900 1 # 900秒内，如果至少有1个key被修改，则执行bgsave ， 如果是save "" 则表示禁用RDB
save 300 10
save 60 10000

RDB的其它配置也可以在redis.conf文件中设置：

rdbcompression yes # 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱

dbfilename dump.rdb # RDB文件名称

dir ./ # 文件保存的路径目录./表示当前运行目录

------------------------------------------------------------

执行bgsave命令时,异步执行RDB的基本流程？
- fork主进程得到一个子进程，共享内存空间
- 子进程读取内存数据并写入新的RDB文件
- 用新RDB文件替换旧的RDB文件。

RDB会在什么时候执行？save 60 1000代表什么含义？
- 默认是服务停止时。
- 代表60秒内至少执行1000次修改则触发RDB

RDB的缺点？
- RDB执行间隔时间长，两次RDB之间写入的数据未被持久化,此时宕机有数据丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时

------------------------------------------------------------

*/

/*
Redis持久化方案二:

AOF全称为Append Only File（追加文件）。Redis处理的每一个写命令都会记录在AOF文件，可以看做是命令日志文件.因为是不断地追加新的写命令到记录文件中,而不是像RBD方式的持久化,每次都要从头开始把整个内存重新写一遍,所以叫(命令日志的)追加文件,显然这种方案
效率更高.

AOF默认是关闭的，需要修改redis.conf配置文件来开启AOF：

appendonly yes # 是否开启AOF功能，默认是no

appendfilename "appendonly.aof" # AOF文件的名称

AOF的命令记录的频率也可以通过redis.conf文件来配：

appendfsync always # 表示每执行一次写命令，立即记录到AOF文件
appendfsync everysec # 写命令执行完先放入AOF缓冲区，然后每隔1秒将缓冲区数据写到AOF文件，是默认方案
appendfsync no # 并非不记录AOF文件,而是写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘

------------------------------------------------------------

AOF文件会比RDB文件大很多,因为AOF文件是记录的操作,每一条操作命令都会记录,RDB只会记录内存中的最终值

而且AOF会记录对同一个key的多次写操作，但只有最后一次写操作才有意义。通过执行bgrewriteaof命令，可以让AOF文件执行重写功能，用最少的命令达到相同效果。

set num 123
set name jack   →bgrewirteaof→   mset name jack num 666          即使如此AOF文件依然比RDB大
set num 666

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置：
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
*/

/*
RDB和AOF各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会结合两者来使用。
                RDB	                                            AOF
持久化方式	    定时对整个内存做快照	                        记录每一次执行的命令
数据完整性	    不完整，两次备份之间会丢失	                    相对完整，取决于刷盘策略
文件大小	    会有压缩，文件体积小	                        记录命令，文件体积很大
宕机恢复速度	很快	                                        慢
数据恢复优先级	低，因为数据完整性不如AOF	                    高，因为数据完整性更高
系统资源占用	高，大量CPU和内存消耗	                        低，主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源
使用场景	    可以容忍数分钟的数据丢失，追求更快的启动速度	对数据安全性要求较高常见
 */

/*
搭建主从架构

------------------------------------------------------------
相关指令

修改redis.conf文件中的一些配置：
# 绑定地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问
bind 0.0.0.0
# 数据库数量，设置为1
databases 1

启动Redis服务器：
redis安装目录下
redis-server <配置文件地址>
redis-server redis.conf

-h：指定连接的Redis服务的ip地址
-p：指定连接的Redis服务的端口号
-a：指定连接的Redis服务的密码

后台启动Redis服务（加&即可）
redis-server redis.conf &

停止redis服务：
redis-cli shutdown
redis-cli -p 端口号 shutdown

打开redis客户端连接redis服务器
redis-cli
redis-cli -h 192.168.138.100 -p 6379 [-a 密码123456]
 -h <hostname>      Server hostname (default: 127.0.0.1).
 -p <port>          Server port (default: 6379).
 -a <password>      Password to use when connecting to the server. You can also use the REDISCLI_AUTH environment variable to pass this password more safely (if both are used, this argument takes precedence).
------------------------------------------------------------


单节点Redis的并发能力是有上限的，要进一步提高Redis的并发能力，就需要搭建主从集群，实现读写分离。
开启主从关系
有临时和永久两种模式：

- 修改配置文件（永久生效）

  - 在redis.conf中添加一行配置：slaveof <masterip> <masterport>

- 使用redis-cli客户端连接到redis服务，执行slaveof命令（重启后失效）：

  slaveof <masterip> <masterport>

注意：在5.0以后新增命令replicaof，与salveof效果一致。

# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 7001

# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001

# 连接 7001
redis-cli -p 7001
# 查看状态
info replication

可以发现，只有在7001这个master节点上可以执行写操作，7002和7003这两个slave节点只能执行读操作。
*/

/*
Redis主从数据同步原理

主从第一次建立连接时，会执行**全量同步**，将master节点的所有数据都拷贝给slave节点
全量同步.png

这里有一个问题，master如何得知salve是第一次来连接呢？？

有几个概念，可以作为判断依据：

- Replication Id：简称replid，是数据集的标记，id一致则说明是同一数据集。每一个master都有唯一的replid，slave则会继承master节点的replid
- offset：偏移量，随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset，说明slave数据落后于master，需要更新。

因此slave做数据同步，必须向master声明自己的replication id 和offset，master才可以判断到底需要同步哪些数据。


因为slave原本也是一个master，有自己的replid和offset，当第一次变成slave，与master建立连接时，发送的replid和offset是自己的replid和offset。

master判断发现slave发送来的replid与自己的不一致，说明这是一个全新的slave，就知道要做全量同步了。

master会将自己的replid和offset都发送给这个slave，slave保存这些信息。以后slave的replid就与master一致了。

因此，master判断一个节点是否是第一次同步的依据，就是看replid是否一致。


#总结
简述全量同步的流程？
1.slave节点请求增量同步
2.master节点判断replid，发现不一致，拒绝增量同步
3.master将完整内存数据生成RDB，发送RDB到slave
4.slave清空本地数据，加载master的RDB
5.master将RDB期间的命令记录在repl_baklog，并持续将log中的命令发送给slave
6.slave执行接收到的命令，保持与master之间的同步

------------------------------------------------------------

全量同步需要先做RDB，然后将RDB文件通过网络传输个slave，成本太高了。因此除了第一次做全量同步，其它大多数时候slave与master都是做**增量同步**。

什么是增量同步？就是只更新slave与master存在差异的部分数据。
增量同步.png

那么master怎么知道slave与自己的数据差异在哪里呢?

repl_backlog原理
这就要说到全量同步时的repl_baklog文件了。
这个文件是一个固定大小的数组，只不过数组是环形，也就是说**角标到达数组末尾后，会再次从0开始读写**，这样数组头部的数据就会被覆盖。
slave与master的offset之间的差异，就是salve需要增量拷贝的数据了。
随着不断有数据写入，master的offset逐渐变大，slave也不断的拷贝，追赶master的offset
直到数组被填满
此时，如果有新的数据写入，就会覆盖数组中的旧数据。不过，旧的数据只要是绿色的，说明是已经被同步到slave的数据，即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。
但是，如果slave出现网络阻塞，导致master的offset远远超过了slave的offset如果master继续写入新数据，其offset就会覆盖旧的数据，直到将slave现在的offset也覆盖
master绕了一圈追尾slave的offset指示的位置后面继续覆盖原有数据的部分，就是尚未同步，但是却已经被覆盖的数据。此时如果slave恢复，需要同步，却发现自己的offset都没有了，无法完成增量同步了。只能做全量同步。


可以从以下几个方面来优化Redis主从就集群：
在master中配置repl-diskless-sync yes启用无磁盘复制，避免全量同步时的磁盘IO。RDB文件不存磁盘而是直接通过网络发送给slave
Redis单节点上的内存占用不要太大，减少RDB导致的过多磁盘IO
适当提高repl_baklog的大小，发现slave宕机时尽快实现故障恢复，尽可能避免全量同步
限制一个master上的slave节点数量，如果实在是太多slave，则可以采用主-从-从链式结构，减少master压力


###总结
简述全量同步和增量同步区别？
全量同步：master将完整内存数据生成RDB，发送RDB到slave。后续命令则记录在repl_baklog，逐个发送给slave。
增量同步：slave提交自己的offset到master，master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步？
slave节点第一次连接master节点时
slave节点断开时间太久，repl_baklog中的offset已经被覆盖时

什么时候执行增量同步？
slave节点断开又恢复，并且在repl_baklog中能找到offset时

*/

/*
Redis哨兵(Sentinel)机制

------------------------------------------------------------
相关指令

# 启动哨兵服务器指令 redis-sentinel <配置文件地址>
redis-sentinel s1/sentinel.conf
------------------------------------------------------------

slave节点宕机恢复后可以找master节点同步数据，那master节点宕机怎么办？

哨兵的作用
Redis提供了哨兵（Sentinel）机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下：
1.监控：Sentinel 会不断检查您的master和slave是否按预期工作
2.自动故障恢复(故障转移)：如果master故障，Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主 ; 如果slave故障,Sentinel也会重启slave
3.通知：Sentinel充当Redis客户端的服务发现来源，当集群发生故障转移时，会将最新信息推送给Redis的客户端


哨兵如何监控各节点服务状态?
Sentinel基于心跳机制监测服务状态，每隔1秒向集群的每个实例发送ping命令：
主观下线：如果某sentinel节点发现某实例未在规定时间响应，则认为该实例主观下线。
客观下线：若超过指定数量（quorum）的sentinel都认为该实例主观下线，则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。


选举新的master
一旦发现master故障，sentinel需要在salve中选择一个作为新的master，选择依据是这样的：
1.首先会判断slave节点与master节点断开时间长短，如果超过指定值（down-after-milliseconds * 10）则会排除该slave节点
2.然后判断slave节点的slave-priority值，越小优先级越高，如果是0则永不参与选举
3.如果slave-prority一样，则判断slave节点的offset值，越大说明数据越新，优先级越高
最后是判断slave节点的运行id大小，越小优先级越高。


如何实现故障转移
当选中了其中一个slave为新的master后（例如slave1），故障的转移的步骤如下：
1.sentinel给备选的slave1节点发送slaveof no one命令，让该节点成为master
2.sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令，让这些slave成为新master的从节点，开始从新的master上同步数据。
3.最后，sentinel将故障节点标记为slave，当故障节点恢复后会自动成为新的master的slave节点


#总结
Sentinel的三个作用是什么？
1.监控
2.故障转移
3.通知

Sentinel如何判断一个redis实例是否健康？
1.每隔1秒发送一次ping命令，如果超过一定时间没有响应则认为是主观下线
2.如果大多数sentinel都认为实例主观下线，则判定服务下线

故障转移步骤有哪些？
1.首先选定一个slave作为新的master，执行slaveof no one
2.然后让所有节点都执行slaveof 新master
3.修改故障节点配置，添加slaveof 新master
 */

/*
Java代码中如何操作?

在Sentinel集群监管下的Redis主从集群，其节点会因为自动故障转移而发生变化，Redis的客户端必须感知这种变化，及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。

引入依赖
在项目的pom文件中引入依赖：
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置sentinel地址
然后在配置文件application.yml中指定redis的sentinel相关信息：
spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.150.101:27001
        - 192.168.150.101:27002
        - 192.168.150.101:27003

配置读写分离
在任意配置类(如项目的启动类)中，添加一个新的bean：
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
    return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
这个bean中配置的就是读写策略，包括四种：
- MASTER：从主节点读取
- MASTER_PREFERRED：优先从master节点读取，master不可用才读取replica
- REPLICA：从slave（replica）节点读取
- REPLICA _PREFERRED：优先从slave（replica）节点读取，所有的slave都不可用才读取master
 */



/*
Redis分片集群---搭建分片集群(分片指的是主节点中一片片的HashSlot散列插槽,Redis中的key不与节点绑定而是与散列插槽绑定,便于主从切换)

问题分析:
redis主从集群可以应对"高并发读",但是主从节点之间需要数据同步,为了提高主从同步时的性能,单节点redis的内存设置不应该太高,如果太高那么在redis进行"Redis Database Backup持久化"或者"从节点全量同步"的时候会导致大量的IO负担,但如果因此降低了单节点Redis内存设置,例如内存只能用10G、20G,那么此时要如何存储海量的数据?

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决：
1.海量数据存储问题
2.高并发写的问题

🔴使用分片集群可以解决上述问题，分片集群特征：
1.集群中有多个master，每个master保存不同数据
2.每个master都可以有多个slave节点
3.master之间通过ping监测彼此健康状态
4.客户端请求可以访问集群任意节点，最终都会被路由转发到正确节点

------------------------------------------------------------

具体操作:

# 进入/tmp目录
cd /tmp
# 删除旧的，避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003

在/tmp下准备一个新的redis.conf文件，内容如下：
port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称，不需要我们创建，由redis自己维护
cluster-config-file /tmp/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /tmp/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.150.101
# 保护模式
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /tmp/6379/run.log

将这个文件拷贝到每个目录下
# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

修改每个目录下的redis.conf，将其中的6379修改为与所在目录一致：
# 进入/tmp目录
cd /tmp
# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

因为已经配置了后台启动模式，所以可以直接启动服务：
# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

通过ps查看状态：
ps -ef | grep redis

如果要关闭所有进程，可以执行命令：
ps -ef | grep redis | awk '{print $2}' | xargs kill
或者（推荐这种方式）:
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown

虽然服务启动了，但是目前每个服务之间都是独立的，没有任何关联。
我们需要执行命令来创建集群，在Redis5.0之前创建集群比较麻烦，5.0之后集群管理命令都集成到了redis-cli中。
我们使用的是Redis6.2.4版本，集群管理以及集成到了redis-cli中，格式如下：
redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003

命令说明：
- `redis-cli --cluster`或者`./redis-trib.rb`：代表集群操作命令
- `create`：代表是创建集群
- `--replicas 1`或者`--cluster-replicas 1` ：指定集群中每个master的副本个数为1，此时`节点总数 ÷ (replicas + 1)` 得到的就是master的数量。因此节点列表中的前n个就是master，其它节点都是slave节点，随机分配到不同master

通过命令可以查看集群状态：
redis-cli --cluster info 192.168.138.150:7001
redis-cli -p 7001 cluster nodes
watch redis-cli -p 7001 cluster nodes 持续动态查看集群状态

尝试连接7001节点，存储一个数据：
# 连接
redis-cli -p 7001
# 存储数据
set num 123
# 读取数据
get num
# 再次存储
set a 1
结果悲剧了：
集群操作时，需要给`redis-cli`加上`-c`参数才可以,-c 代表cluster集群模式
redis-cli -c -p 7001
这次可以了
 */

/*
Redis分片集群---散列插槽(hash slot)

Redis会把每一个master节点映射到0~16383共16384个插槽（hash slot）上，查看集群信息(redis-cli -p 7001 cluster nodes)时就能看到
数据key不是与节点绑定，而是与插槽绑定。redis会根据key的有效部分计算插槽值，分两种情况：
1.key中包含"{}"，且“{}”中至少包含1个字符，“{}”中的部分是有效部分
2.key中不包含“{}”，整个key都是有效部分
例如：key是num，那么就根据num计算，如果是{itcast}num，则根据itcast计算。计算方式是利用CRC16算法得到一个hash值，然后对16384取余，得到的结果就是slot值。


# 总结
Redis如何判断某个key应该在哪个master节点？
1.将16384个插槽分配到不同的master节点
2.根据key的有效部分计算哈希值，对16384取余
3.余数作为插槽，寻找插槽所在master节点即可

如何将同一类数据固定的保存在同一个master节点？
这一类数据使用"有效部分相同的key"，例如key都以{wtp}为前缀:{wtp}a {wtp}b {wtp}c
 */

/*
Redis分片集群---集群伸缩(添加删除节点)

redis-cli --cluster提供了很多操作集群的命令，可以通过下面方式查看：
redis-cli --cluster help

添加一个节点到集群:
redis-cli --cluster  add-node   new_host:new_port existing_host:existing_port
            后面还可以带上两个参数,指定该新增节点为从节点,并指定其主节点的ID,如果不带这两个参数则默认新增节点是主节点
             --cluster-slave
             --cluster-master-id <arg>


插槽转移:
redis-cli --cluster  reshard   host:port
             --cluster-from <arg>
             --cluster-to <arg>
             --cluster-slots <arg>
             --cluster-yes
             --cluster-timeout <arg>
             --cluster-pipeline <arg>
             --cluster-replace


#案例#
向集群中添加一个新的master节点，并向其中存储 num = 10
需求：
启动一个新的redis实例，端口为7004
添加7004到之前的集群，并作为一个master节点
给7004节点分配插槽，使得num这个key可以存储到7004实例


添加7004,默认是一个master节点
redis-cli --cluster add-node  192.168.150.101:7004 192.168.150.101:7001
查看集群状态
redis-cli -p 7001 cluster nodes
但是，可以看到7004节点的插槽数量为0，因此没有任何数据可以存储到7004上
我们要将num存储到7004节点，因此需要先get num看看num的插槽是多少,num的插槽为2765.
我们可以将0~3000的插槽从7001转移到7004，命令格式如下：
redis-cli --cluster reshard 192.168.138.150:7001
询问要移动多少个插槽，输入3000
询问接收插槽的节点ID,复制粘贴7004的ID
询问插槽来源节点ID,复制粘贴7001的ID,enter,再次输入done完成
再次查看集群状态
redis-cli -p 7001 cluster nodes
然后redis-cli -c -p 7001
get num查看原来num的值,会发现2765插槽已经在7004节点上了
set num 10设置成功

#案例#
删除集群中的一个节点
需求：
删除7004这个实例
redis-cli --cluster del-node host:port node_id
redis-cli --cluster del-node 192.168.138.150:7004 934648f119ada98c7c4d170e3349d2ccfcd2f51a
 */
/*
Redis分片集群---故障转移

故障转移(主从转换)
当集群中有一个master宕机会发生什么呢？
1.首先是该节点与其它节点失去连接
2.然后是疑似宕机
3.最后是确定下线，自动提升一个slave为新的master
4.如果恢复上线后,将变成一个slave节点



手动数据迁移(手动主从切换)
当需要用某个新节点替换旧节点成为master节点时.先让某个slave节点跟从集群中的某个master节点,再在slave节点上利用cluster failover命令手动让master宕机，master与slave节点身份将互换,实现无感知的数据迁移。

手动故障转移.png

手动的Failover支持三种不同模式：
缺省：默认的流程，如图1~6歩
force：省略了对offset的一致性校验
takeover：直接执行第5歩，忽略数据一致性、忽略master状态和其它master的意见

# 案例
在7002这个slave节点执行手动故障转移，重新夺回master地位
步骤如下：
1.利用redis-cli连接7002这个节点
2,执行cluster failover命令

 */

/*
Java代码操作分片集群---RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持，而使用的步骤与哨兵模式基本一致：
1.引入redis的starter依赖
2.配置分片集群地址
3.配置读写分离
与哨兵模式相比，其中只有分片集群的配置方式略有差异，如下：
spring:
  redis:
      cluster:
          nodes: # 指定分片集群的每一个节点信息
                  - 192.168.150.101:7001
                  - 192.168.150.101:7002
                  - 192.168.150.101:7003
                  - 192.168.150.101:8001
                  - 192.168.150.101:8002
                  - 192.168.150.101:8003


*/
}

